# Модуль 5. Кортежи, словари и множества


На этом занятии мы с вами познакомимся с еще одной новой коллекцией данных – кортежами. 

Что такое кортеж? Это упорядоченная, но неизменяемая `(immutable)` коллекция произвольных данных. 

Когда вы присваиваете кортежу элемент, он «запекается» и больше не изменяется.

Они, в целом, аналогичны спискам, но в отличие от них, элементы кортежей менять нельзя – это неизменяемый тип данных. 

Т.е. мы не можем добавлять или удалять элементы в кортеже, изменять его


```python
a = 1,2
a = (1, 2, 3)

a, b, c = 3, 'Hello', True
print(a,b,c)

# b = 1,     или    b = (1,)

a[1] = 100
lst=[1,2,3]
t = (1,2,3)
print(lst.__sizeof__())
print(t.__sizeof__())

a = ()
a = a +  (1,)

a += (("a", "hello"),)

b = (0,)*10
b = ("hello", "world") * 5

del a[1]

del a

a = tuple([1,2,3])
a = tuple("Привет мир")
print(f'{a = }')

a = (True, [1,2,3], "hello", 5, {"house": "дом"})

tuple.count(значение) 
tuple.index(значение[, start[, stop]]) 

s = 'Python'

for item in enumerate(s):
    print(item)

```

## Вычислительная сложность структур данных

```python
import platform
print (platform.processor() , platform.architecture())

import time

start = time.time()
x = [i for i in range(10_000_000)]
end = time.time()
print(end - start)

start = time.time()
x = (i for i in range(10_000_000))
end = time.time()
print(end - start)

start = time.time()
x = {i for i in range(10_000_000)}
end = time.time()
print(end - start)

start = time.time()
x = range(10_000_000)
end = time.time()
print(end - start)

```

Результат:
```
Intel64 Family 6 Model 142 Stepping 10, GenuineIntel ('64bit', 'WindowsPE')
0.695237398147583
0.08672451972961426
0.5249037742614746
0.12475299835205078
```

## Распаковка последовательностей

```python
a = [1,2,3,4]

print (*a, sep=',')

s1,s2,s3,s4,s5 = 'hello'

print(s1)
s1,s2,s3,*s4 = 'hello'

print(s4) #['l', 'o']
s = 'hello python'
a, b = s.split()
print(b, a)

R = range(4,7)
a,b,c = R
print(a)
# Звездочка может быть
a, *b, c = s

print(b)

s1, *s2, s3 = 'hello'

print(s2) #['e', 'l', 'l']

s = 'hello python'

_, _, c, *d, e = s

print(d)

s = '1234' # Какое минимальное количество элементов

_, _, c, *d, e = s

print(d)
```

## Словари

### Базовые операции

```python
s = 'Python'  # строка (str)
l = [3, 7, 5.5, 'Python', True]  # список  (list)
t = (3, 7, 5.5, 'Python', True) # множество  (tuple)
r = range(20) # прогрессия (range)
```

Но представим, что мы должны шарики раскладывать по цветам, каждый в свою коробочку.
```
# red
# blue
# yellow
# green
```

Красные в нулевую, синие во вторую, желтые в третью и т.д.

Но как только шариков станет много 7±2 человеческая память начнет давать сбой.

Мы начнем забывать под каким индексом где-какие шарики лежат.

Т.е. если цветов станет 20, мы будем путаться с коробочками.

А если коробочки подписать, то всё встанет на место. Человек проще с именами.

ball_lst = `[5,7,11,10]`

Поэтому во всех языках программирования присутствуют ассоциативные списки, хеш-таблицы (ключ-значение), мапы, dict.


Словарь в Python (он же ассоциативный список) — это тот-же список, но с небольшими отличиями. 

Индексов нет. 

Точнее, в качестве индекса (который в словаре будет называться ключ) могут выступать не только числа, но и любые другие типы данных (даже другие коллекции!). 

Грубо говоря, имена и значения.

А вместо индексов – ключи! 
```python
balls = {'red': 5, 'blue': (1, 2, 3), 'green': [10, 11]}
print(balls)
```

Н уровне структуры данных ключи словаря – это неповторяющиеся значения.

Ключи уникальны, а значения могут повторяться.

Таким образом, ключи это? Неизменяемые типы данных! Какие?

`str, int, float, bool, tuple, range`

Значение словаря? Любые типы данных!

С точки зрения

Словарь изменяемый тип данных.

Когда мы в памяти переменные создаем.

Где они хранятся? 

С словаре! Имя переменной и есть ключ. 

Гарантированно!

Словарь легко преобразуется в json. 

Раньше был xml, но пик популярности у него прошел.

Обращение к значениям словаря происходит по ключам

```python
print(balls['blue'])
```
Если ключа нет, мы получим ошибку
Где коллизия?

```python
balls = {'red': 5, 'blue': (1, 2, 3), 'blue': [10, 11]}
print(balls)
print(balls['blue'])
```
Какое значение получим по ключу blue?
А сейчас?
```python

balls = {
    'red': 5,
    'blue': (1, 2, 3),
    'blue': [10, 11],
    'blue': {'red': 5, 'blue': (1, 2, 3), 'blue': [10, 11]}
}
print(balls)
print(balls['blue'])
А в такой ситуации
print(balls)
print(balls['blue']['blue'])
print(balls['blue'])
```

Также допустимы пропуски, если мы все-таки будем использовать в качестве ключа целое число, например у нас может быть элемент связанный с ключем 5, но при этом отсутствовать элемент связанный с ключем 4.

Что все это значит на практике? 

Всего-лишь, то, что в квадратных скобках для обращения к элементу по “индексу” мы можем указывать произвольный тип, например `allMyCats[“Murka”]`.

Давайте поэкспериментируем с шарами. 

С учетом знаний типов данных

```python
balls = {
    'red': 5,
    'blue': (1, 2, 3),
    'blue': [10, 11],
    'blue': {'red': 5, 'blue': (1, 2, 3), 'blue': [10, 11]}
}
```

Что мы увидим в консоли
```python

balls['red'] += 5
balls['blue'] += 5
balls['blue']['red'] += 5
balls['black'] = 10

print(balls)

```

Далее 

Вернемся в коробочка с шарами
```python

balls = {
    'red': 5,
    'blue': 1,
    'green': 2,
    'yellow': 0
}
```
Поищем синие шары
```python

print ('blue' in balls)
print(balls.get('pink', 'Нет такого ключа'))
```

Следующая ситуация

```python
if 'white' not in balls:
    balls['white'] = 1
else:
    balls['white'] = 1

print(balls)


pprint.pprint(balls, width=20)
```

Без знания физики мир становится удивительным и чудесным.

Без знания химии, человек верит в любовь, а со знанием биохимии гормонов строит долгий и счастливый брак.

Так же и в программировании. Чем дальше, тем меньше магии и волшебства.

Одни и те же механизмы.

Практическая задача: Найти частоту вхождения каждого символа в строку

‘Hello, Python’ 
```python

str_ = 'Hello, Python'.replace(' ','')

count_char = {}

for char in str_:
    if char in count_char:
        count_char[char] += 1
    else:
        count_char[char] = 1


pprint.pprint(count_char, width=20)
```

Еще короче
```python

for char in str_:
    if not char in count_char:
        count_char[char] = str_.count(char)

# или     if char not in count_char:
```
Давайте представим, что мы в программе хотели бы описать, следующие зависимости:
```
house -> дом
car -> машина
tree -> дерево
road -> дорога
river -> река
```
Если определить значения через список:
```python
d = ["дом", "машина", "дерево", "дорога", "река"]
```

## [Задания на словари-1](/module-5/1_practice.md)
Методы словаря, перебор элементов словаря в цикле

Мы продолжаем изучение словарей языка Python.

Это занятие начнем с ознакомления основных методов, которые есть у этой коллекции. 

Посмотрим на 

```python
d = {'one': '1', 'two': '2', 'three': '3'}
print(d.values())
print(d.keys())
print(d.items())
Результат:
dict_values(['1', '2', '3'])
dict_keys(['one', 'two', 'three'])
dict_items([('one', '1'), ('two', '2'), ('three', '3')])
```

Метод `items` возвращает множество-подобный `(set-like)`, состоящий из уникальных не повторяющихся элементов, объект типа `dict_items`, состоящий из списка кортежей пар ключ-значение.

Метод `keys` возвращает множество-подобный `(set-like)`, состоящий из уникальных не повторяющихся элементов, объект типа `dict_keys`, состоящий из списка ключей словаря.

Метод `values возвращает` множество-подобный `(set-like)`, состоящий из уникальных не повторяющихся элементов, объект типа `dict_values`, состоящий из списка значений словаря.

```python
d = {'one': '1', 'two': '2', 'three': '3'}
it = d.items()
print(it)
del d['one']
print(it)
# Результат:
dict_items([('one', '1'), ('two', '2'), ('three', '3')])
dict_items([('two', '2'), ('three', '3')])6
```

Соответственно, мы можем и дальше преобразовывать эти структуры данных:

```python
d = {'one': '1', 'two': '2', 'three': '3'}
vals = d.values()

print(list(vals)[0)
и так далее
vals = d.values()
keys = d.keys()
items = d.items()

print(list(vals))
print(list(keys))
print(list(items))
['1', '2', '3']
['one', 'two', 'three']
[('one', '1'), ('two', '2'), ('three', '3')]
```

Т.е. изначально работать с такими set-подобными структурами мы не можем, а после преобразования – легко!
И даже так

```python
print(dict(items))
# получим исходный словарь
{'one': '1', 'two': '2', 'three': '3'}
```
Начнем с метода

`dict.fromkeys(список[, значение по умолчанию])`

который формирует словарь с ключами, указанными в списке и некоторым значением. Например, передадим методу список с кодами стран:
```python
a = dict.fromkeys(["+7", "+6", "+5", "+4"])
```

в результате, получим следующий словарь:
```python
{'+7': None, '+6': None, '+5': None, '+4': None}
```
Обратите внимание, все значения у ключей равны `None`. 

Это значение по умолчанию. 

Чтобы его изменить используется второй необязательный аргумент, например:

```python
a = dict.fromkeys(["+7", "+6", "+5", "+4"], "код страны")
```
на выходе получаем словарь с этим новым указанным значением:
```python
{'+7': 'код страны', '+6': 'код страны', '+5': 'код страны', '+4': 'код страны'}
```

Преобразуем список в словарь при помощи генератора словаря

Для преобразования списка `Python` в словарь также можно использовать генератор словаря.

Генератор словаря похож на генератор списка в том, что оба они создают новое значение соответствующего типа данных.

```python
fruits = ["Apple", "Pear", "Peach", "Banana"]
fruit_dictionary = {fruit : "In stock" for fruit in fruits}
print(fruit_dictionary)
```

Для начала мы объявили список фруктов `(fruits)`, где хранятся их названия, которые мы хотим перенести в словарь.

Затем мы использовали генератор словаря, чтобы пройтись по каждому элементу в списке `fruits`. 

Для каждого фрукта в нашем списке мы добавили элемент в новый словарь. 

При этом каждому фрукту мы присвоили значение `In stock`.

Наконец, мы вывели наш новый словарь в консоль. 

Можем запустить наш код, и он выведет то же самое, что и в первом примере.

Конвертируем два списка в словарь при помощи `zip()`

В нашем последнем примере мы преобразовываем в словарь один список и присваиваем одинаковое значение по умолчанию для каждого ключа в словаре.

 Но можно преобразовать в словарь и два списка. 
 
 Давайте попробуем это сделать.

Для этого мы можем использовать такую функцию `Python`, как `zip()`.

Она позволяет объединить два списка.
 
Элементы одного списка мы можем использовать в качестве ключей для словаря, а элементы второго — в качестве значений.

Итак, предположим, что у нас есть два списка: один содержит названия фруктов, а другой – их цены.

Мы хотим создать единый словарь, в котором будет храниться название фрукта и его цена.

Для выполнения этой задачи мы можем использовать следующий код:

```python
fruits = ["Apple", "Pear", "Peach", "Banana"]
prices = [0.35, 0.40, 0.40, 0.28]
fruit_dictionary = dict(zip(fruits, prices))
print(fruit_dictionary)
```

Что же мы сделали? 

Во-первых, мы определили два списка: список фруктов fruits и список цен prices.

Затем мы использовали функцию `zip()`, чтобы объединить наши списки. 

Эта функция возвращает список кортежей. Поскольку нам нужен словарь, для преобразования наших кортежей мы использовали dict().
В конце мы вывели содержимое нашего нового словаря в консоль.

Проверим себя
```python
months = 'Сентябрь', 'Октябрь', 'Ноябрь', 'Сентябрь'
days = 1, 2, 3

res = dict(zip(months,days))

print(len(res))

# Какая длина словаря 

# Результат
{'Сентябрь': 1, 'Октябрь': 2, 'Ноябрь': 3}
# А если вот так
days = 1, 2, 3, 4
# А сейчас какая длина у словаря
months = 'Сентябрь', 'Октябрь', 'Сентябрь', 'Ноябрь', 'Декабрь'
days = 1, 2, 3, 4

res = dict(zip(months,days))
```
В случае с кортежами или списками, ситуация была бы другая.

Следующий метод

d.clear()

служит для очистки словаря, то есть, удаления всех его элементов.

Далее, для создания копии словаря используется метод copy:
```python
d = {True: 1, False: "Ложь", "list": [1,2,3], 5: 5}
d2 = d.copy()
d2["list"] = [5,6,7]
print(d)
print(d2)
```

Также копию можно создавать и с помощью функции `dict()`, о которой мы с вами говорили на предыдущем занятии:
```python
d2 = dict(d)
```

Результат будет абсолютно таким же, что и при вызове метода `copy()`.

Следующий метод get позволяет получать значение словаря по ключу:
```python
d.get("list")
```
Его отличие от оператора

```python
d["list"]
```

в том, что при указании неверного ключа не возникает ошибки, а выдается по умолчанию значение `None`:
```python
print(d.get(3))
```

Это значение можно изменить, указав его вторым аргументом:
```python
print( d.get(3, False) )
```
Похожий метод
```python
dict.setdefault(key[, default])
```

возвращает значение, ассоциированное с ключом key и если его нет, то добавляет в словарь со значением None, либо default – если оно указано:
```python
d.setdefault("list")
d.setdefault(3)
# Добавит ключ 3 со значением None. Удалим его:
del d[3]
d.setdefault(3, "three")
```

тогда добавится этот ключ со значением «three». 

То есть, этот метод способен создать новую запись, но только в том случае, если ключ отсутствует в словаре.
Следующий метод
```python
d.pop(3)
```

удаляет указанный ключ и возвращает его значение.

Если в нем указывается несуществующий ключ, то возникает ошибка:
```python
d.pop("abc")
```

Но мы можем легко исправить ситуацию, если в качестве второго аргумента указать значение, возвращаемое при отсутствии ключа:
```python
d.pop("abc", False)
```

Здесь возвратится `False`. 
Если же ключ присутствует, то возвращается его значение.
Следующий метод

```python
d.popitem()
```
выполняет удаление произвольной записи из словаря. 

Если словарь пуст, то возникает ошибка:
```python
d2 = {}
d2.popitem()
Следующий метод
d.keys()
```

возвращает коллекцию ключей. 

По умолчанию цикл for обходит именно эту коллекцию, при указании словаря:
```python
d = {True: 1, False: "Ложь", "list": [1,2,3], 5: 5}
for x in d:
    print(x)
```
то есть, эта запись эквивалента такой:

```python
for x in d.keys():
    ...
# Если же вместо keys записать метод values:
for x in d.values():
    ...
# то обход будет происходить по значениям, то есть, метод values возвращает коллекцию из значений словаря.
# Последний подобный метод items
for x in d.items():
    ...
```
возвращает записи в виде кортежей: ключ, значение. 

О кортежах мы уже говорили, здесь лишь отмечу, что к элементу кортежа можно обратиться по индексу и вывести отдельно ключи и значения:
```python
print(x[0], x[1])
```

Или, используя синтаксис множественного присваивания:
```python
x,y = (1, 2)
```

можно записать цикл for в таком виде:
```python
for key, value in d.items():
    print(key, value)
```
что гораздо удобнее и нагляднее.

Следующий метод update() позволяет обновлять словарь содержимым другого словаря. 

Например, есть два словаря:
```python
d = dict(one = 1, two = 2, three = "3", four = "4")
d2 = {2: "неудовлетворительно", 3: "удовлетворительно", 'four': "хорошо", 5: "отлично"}
```

И мы хотим первый обновить содержимым второго:
```python
d.update(d2)
```

Смотрите, ключ four был заменен строкой «хорошо», а остальные, не существующие ключи были добавлены.

Еще способ, доступный в Pt с версии 3.9
```python
d1 = {'Сентябрь': 10, 'Октябрь': 11, 'Ноябрь': 12}
d2 = {'Январь': 1, 'Февраль': 2, 'Март ': 3, 'Сентябрь': 100}

d3 = d1 | d2
d1.update(d2)

print(d3)
print(d1)
```

Или 
Если же нам нужно создать новый словарь, который бы объединял содержимое обоих, то для этого можно воспользоваться конструкцией:
```python
d3 = {**d1, **d2}
```

Однако, детально как она работает будет понятно позже, когда мы познакомимся с операторами распаковки коллекций.
```python
holidays = {
    'January': (1, 2, 3, 4, 5, 6, 7, 8),
    'February': (23),
    'March': (8)
}
```
Что мы увидим в принте?
```python
print(holidays['January'][0])
А в этом случае
holidays = {
    1: (1, 2, 3, 4, 5, 6, 7, 8),
    2: (23),
    3: (8)
}

print(holidays[1][0])

# А сейчас как вытащить цифру 1

holidays = {
    (1,2) : (1, 2, 3, 4, 5, 6, 7, 8),
    2: (23),
    3: (8)
}
```
Итак, на этом занятии мы с вами познакомились с основными методами словарей, а также способами их перебора и объединения. 


## [Задания на словари-2](/module-5/1_practice.md)



# Множества (set) и их методы

На этом занятии мы с вами познакомимся с последней базовой коллекцией – множествами.

Формально, множество (set) – это неупорядоченная коллекция уникальных элементов. 

Уникальных, то есть, в ней отсутствуют дублирующие значения.

Для задания множества используются фигурные скобки, в которых через запятую перечисляются значения элементов:
```python
a = {1, 2, 3, "hello"}
```

Это немного похоже на определение словаря, но в отличие от него, здесь не прописываются ключи – только значения. 

В результате получаем совершенно другой тип объекта – множество:

type(a)

Так как множество состоит только из уникальных значений, то попытка в него поместить несколько одинаковых значений:
```python
a = {1, 2, 3, "hello", 2, 3, "hello"}
```
приведет к тому, что останутся только не повторяющиеся. 

Это ключевая особенность работы данной коллекции – она автоматически отбрасывает все дубли.


Во множество мы можем записывать только неизменяемые данные, такие как:
* числа;
* булевы значения;
* строки;
* кортежи

```python
a = {1, 4.5, "hi", "hi", (4, True), ("a", False)}
```
А вот изменяемые типы данных:

* списки;
* словари;
* множества

добавлять нельзя, будет ошибка:

```python
b = {[1, 2], 3, 4}
b = {{1: 1, 2: 2}, 3, 4}
b = {{1, 2}, 3, 4}
```

Для создания множества используется встроенная функция `set()`. 

Если ее записать без аргументов:
```python
set()
```

то получим пустое множество. 

Но, обратите внимание, если попытаться создать пустое множество вот так:

b = {}  # словарь

то получим не пустое множество, а пустой словарь! 

Пустое множество создается именно с помощью функции set(). 

Вот этот момент нужно запомнить.

Если указать какой-либо итерируемый объект, например, список:
```python
set([1, 2, 1, 0, -1, 2])
```

то получим множество, состоящее из уникальных значений этого списка. 

Или, если передать строку:
```python
set("abrakadabra")
```

то увидим множество из уникальных символов этой строки. 

И так для любого итерируемого объекта, даже можно прописать функцию range():
```python
b = set(range(7))
```

Обратите внимание, множества представляют собой неупорядоченную коллекцию, то есть, значения элементов могут располагаться в них в любом порядке. 

Это также означает, что с множествами нельзя выполнять операции доступа по индексу или срезам:
```python
b[0]
```

приведет к ошибке.

Для чего вообще может потребоваться такая коллекция в программировании? 

Я, думаю, большинство из вас уже догадались – с их помощью, например, можно легко и быстро убирать дубли из данных. 

Представим, что некоторая логистическая фирма составила список городов, куда доставляли товары:
```python
cities = ["Калуга", "Краснодар", "Тюмень", "Ульяновск", "Москва", "Тюмень", "Калуга", "Ульяновск"]
```

И отдел статистики хотел бы получить список уникальных городов, с которыми было сотрудничество.

Очевидно, это можно сделать с помощью множества:

set(cities)

Обратите внимание, порядок городов может быть любым, так как множество – неупорядоченная коллекция данных. 

Мало того, мы можем этот полученный перечень снова преобразовать в список, используя функцию:
```python
list(set(cities))
```

Все, мы отобрали уникальные города и представили их в виде списка. 

Видите, как это легко и просто можно сделать с помощью множеств.


А что если нам не нужен список уникальных городов, а достаточно лишь перебрать все элементы полученного множества:
```python
q = set(cities)
```
Учитывая, что множество – это итерируемый объект, то пройтись по всем его элементам можно с помощью цикла for:
```python
for c in q:
     print(c)
```

Здесь переменная c последовательно ссылается на значения элементов множества q и соответствующий город выводится в консоль. 

Или, то же самое можно сделать через механизм итераторов:
```python
it = iter(q)
next(it)
```
Наконец, функция:
```python
a = {"abc", (1, 2), 5, 4, True}
len(a)
```

возвратит нам число элементов в множестве. 

А для проверки наличия значения в множестве можно воспользоваться, уже знакомым нам оператором in:

`"abc" in a`

Он возвращает True, если значение имеется и False в противном случае. 

Или можно проверить на непринадлежность какого-либо значения:
```python
7 not in a
```

Методы добавления/удаления элементов множества

В заключение этого занятия рассмотрим методы для добавления и удаления элементов в множествах. 

Первый метод `add()` позволяет добавлять новое значение в множество:
```python
b.add(7)
```

Значение будет добавлено только в том случае, если оно отсутствует в множестве b.

Если попробовать, например, еще раз добавить семерку:
```python
b.add(7)
```

то множество не изменится, но и ошибок никаких не будет.

Если в множество требуется добавить сразу несколько значений, то для этого хорошо подходит метод `update()`:
```python
b.update(["a", "b", (1, 2)])
```

В качестве аргумента указывается любой итерируемый объект, в данном случае, список, значения которого добавляются в множество с проверкой уникальности. 

Или, можно передать строку:
```python
b.update("abrakadabra")
```


Получим добавку в виде уникальных символов этой строки.

 И так далее, в качестве аргумента метода `update()` можно указывать любой перебираемый объект.

Далее, для удаления элемента по значению используется метод `discard()`:
```python
b.discard(2)
```

Если еще раз попытаться удалить двойку:
```python
b.discard(2)
```

то ничего не произойдет и множество не изменится.

Другой метод для удаления элемента по значению – `remove()`:
```python
b.remove(4)
```

но при повторном таком вызове:
```python
b.remove(4)
```

возникнет ошибка, т.к. значение 4 в множестве уже нет. 

То есть, данный метод возвращает ошибку при попытке удаления несуществующего значения. 

Это единственное отличие в работе этих двух методов.

Также удалять элементы можно и с помощью метода pop:
```python
b.pop()
```

При этом он возвращает удаляемое значение, а сам удаляемый элемент оказывается, в общем-то, случайным, т.к. множество – это неупорядоченная коллекция. 
Если вызвать этот метод для пустого множества, то возникнет ошибка:

```python
c=set()
c.pop()
```

Наконец, если нужно просто удалить все элементы из множества, то используется метод:
```python
b.clear()
```
На этом мы с вами завершим первое знакомство с множествами. 


## [Задания на множества - 1](/module-5/1_practice.md)

## Операции над множествами, сравнение множеств

На этом занятии мы с вами будем говорить об операциях над ними – это:

* пересечение множеств;
* объединение множеств;
* вычитание множеств;
* вычисление симметричной разности.

Для простоты зададим два множества с числами:

```python
setA = {1, 2, 3, 4}
setB = {3, 4, 5, 6, 7}
```
На их основе можно построить третье множество, как результат пересечения этих двух:
`setA & setB`

![alt text](img/set1.png)

На выходе получаем новое множество, состоящее из значений, общих для множеств `setA` и `setB`.

Это и есть операция пересечения двух множеств.

При этом, исходные множества остаются без изменений. 
 
Здесь именно создается новое множество с результатом выполнения этой операции.

Разумеется, чтобы в дальнейшем в программе работать с полученным результатом, его нужно сохранить через какую-либо переменную, например, так:
```python
res = setA & setB

#Также допустима запись в виде:

setA &= setB
# это будет эквивалент строчки:
setA = setA & setB
```

То есть, мы вычисляем результат пересечения и сохраняем его в переменной seta и, как бы, меняем само множество setA.
А вот если взять множество:
```python
setC = {9, 10, 11}
# которое не имеет общих значений с другим пересекающимся множеством, то результатом:
setA & setC
```
будет пустое множество.

Обычно, на практике используют оператор пересечения &, но вместо него можно использовать специальный метод:
```python

setA = {1, 2, 3, 4}
setB = {3, 4, 5, 6, 7}
setA.intersection(setB)
```

Результат будет тем же, на выходе получим третье множество, состоящее из общих значений множеств setA и setB. 

Если же мы хотим выполнить аналог операции:
```python
setA &= setB
# то для этого следует использовать другой метод:
setA.intersection_update(setB)
```

В результате, меняется само множество setA, в котором хранятся значения пересечения двух множеств.

Вот такие способы вычислений пересечения множеств существуют в Python.

### Объединение множеств

Следующая операция – это объединение двух множеств.

Она записывается, так:
```python
setA = {1, 2, 3, 4}
setB = {3, 4, 5, 6, 7}
setA | setB
```

На выходе также получаем новое множество, состоящее из всех значений обоих множеств, разумеется, без их дублирования.

![alt text](img/set2.png)

Или же можно воспользоваться эквивалентным методом:

```python
setA.union(setB)
```
который возвращает множество из объединенных значений. 

При этом, сами множества остаются без изменений.
Операцию объединения можно записать также в виде:
```python
setA |= setB
```
Тогда уже само множество `setA` будет хранить результат объединения двух множеств, то есть, оно изменится. 

Множество `setB` останется без изменений.

### Вычитание множеств

Следующая операция – это вычитание множеств. Например, для множеств:
```python
setA = {1,2,3,4}
setB = {3,4,5,6,7}
# операция
setA - setB
```
возвратит новое множество, в котором из множества `setA` будут удалены все значения, повторяющиеся в множестве `setB`:
`{1, 2}`
 
Или, наоборот, если из множества setB вычесть множество `setA`:
```python
setB - setA
```

то получим значения из которых исключены величины, входящие в множество `setA`.

![alt text](img/set3.png)

Также здесь можно выполнять эквивалентные операции:

```python
setA -= setB
setB -= setA
```

В этом случае переменные `setA` и `setB` будут ссылаться на соответствующие результаты вычитаний.

### Симметричная разность

Наконец, последняя операции над множествами – симметричная разность, возвращает множество, состоящее из неповторяющихся значений обоих множеств. 
Реализуется она следующим образом:

```python
setA = {1,2,3,4}
setB = {3,4,5,6,7}
setA ^ setB
```

![alt text](img/set4.png)
На выходе получим третье, новое множество, состоящее из значений, не входящих одновременно в оба множества.
 
### Сравнение множеств

![alt text](img/set5.png)

Предположим, имеются два таких множества:
```python
setA = {7, 6, 5, 4, 3}
setB = {3, 4, 5, 6, 7}
```

И мы хотим узнать, равны они или нет. 

Для этого, используется уже знакомый нам оператор сравнения на равенство:
```python
setA == setB
```
Увидим значение True (истина). 
Почему? Дело в том, что множества считаются равными, если все элементы, входящие в одно из них, также принадлежат другому и мощности этих множеств равны (то есть они содержат одинаковое число элементов). 

Наши множества `setA и setB` удовлетворяют этим условиям.

Соответственно, противоположное сравнение на не равенство, выполняется, следующим образом:

`setA != setB`

Следующие два оператора сравнения на больше и меньше, фактически, определяют, входит ли одно множество в другое или нет.
 
Например, возьмем множества
```python
setA = {7, 6, 5, 4, 3}
setB = {3, 4, 5}
#тогда операция
setB < setA
#вернет True, а операция
setB > setA
# значение False. Но, если хотя бы один элемент множества setB не будет принадлежать множеству setA:
setB.add(22)
# то обе операции вернут False. Для равных множеств
setA = {7, 6, 5, 4, 3}
setB = {3, 4, 5, 6, 7}
# обе операции также вернут False. Но вот такие операторы:
setA <= setB
setA >= setB
```
вернут True.

Я, думаю, из этих примеров понятно, как выполняется сравнение множеств между собой.



## Frozenset (неизменяемое множество)

```python
a = frozenset('hellow')
b = set('hellow')
print(a == b)
print(type(a - b))
print(type(b | a))
b.add('q')
# a.add('q') # вызовет ошибку
```

[Задания на множества - 2](/module-5/1_practice.md)
